/*
 * Copyright (C) 2025 ameise <ameise.wang@gmail.com> - All Rights Reserved
 *
 * This file is part of e3lock.
 *
 * e3lock is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * e3lock is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with e3lock. If not, see <https://www.gnu.org/licenses/>.
 */

//go:build debug

package e3lock

import (
	"fmt"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

type stack struct {
	file     string
	funcName string
	line     int
	gid      int
	skip     int
	t        time.Time
}

type SpinLock struct {
	o     sync.Once
	mu    sync.Mutex
	infos []stack
}

func (ts *SpinLock) TryLock() bool {
	return ts.tryLock(1)
}

func (ts *SpinLock) Lock() {
	ts.lock(1)
}

func (ts *SpinLock) Unlock() {
	ts.unlock(1)
}

func (ts *SpinLock) tryLock(skip int) bool {
	ok := ts.mu.TryLock()
	if ok {
		ts.recordLock(getGoID(), 1+skip)
	}
	return ok
}

func (ts *SpinLock) lock(skip int) {
	const maxBlock = 64
	block := 1
	t0 := time.Now()
	t1 := t0
	gid := getGoID()

	for {
		if ts.mu.TryLock() {
			break
		}
		for i := 0; i < block; i++ {
			runtime.Gosched()
		}
		if block < maxBlock {
			block <<= 1
		}
		if _gLogger != nil {
			t2 := time.Now()
			if t2.Sub(t1).Seconds() > 5 {
				t1 = t2
				funcName, file, line, _ := runtime.Caller(1 + skip)
				b := strings.Builder{}
				for x, info := range ts.infos {
					b.WriteString(fmt.Sprintf("\t-> [%d][gid:%d] func: %s\t%s:%d time:%s\n", x, info.gid, info.funcName, filepath.Base(info.file), info.line, info.t.Format(layout)))
				}
				_gLogger.WarnPrintf("dead lock[%.2f s]\nlock on:\n%s\ncalling on:\n\t[gid:%d] func: %s\t%s:%d\n", t2.Sub(t0).Seconds(), b.String(), gid, runtime.FuncForPC(funcName).Name(), filepath.Base(file), line)
			}
		}
	}
	ts.recordLock(gid, 1+skip)
}

func (ts *SpinLock) unlock(skip int) {
	ts.mu.Unlock()
	ts.recordUnLock(getGoID(), 1+skip)
}

func (ts *SpinLock) recordLock(gid int, skip int) {
	funcName, file, line, _ := runtime.Caller(1 + skip)
	info := stack{file: file, funcName: runtime.FuncForPC(funcName).Name(), line: line, gid: gid, skip: skip, t: time.Now()}
	if _gLogTrace && _gLogger != nil {
		b := strings.Builder{}
		b.WriteString(fmt.Sprintf("[%d][gid:%d] func: %s\t%s:%d", len(ts.infos), info.gid, info.funcName, filepath.Base(info.file), info.line))
		_gLogger.TracePrintf("lock on: %s\n", b.String())
	}
	ts.infos = append(ts.infos, info)
}

func (ts *SpinLock) recordUnLock(gid int, skip int) {
	ts.infos = ts.infos[:0]
	if _gLogTrace && _gLogger != nil {
		funcName, file, line, _ := runtime.Caller(1 + skip)
		b := strings.Builder{}
		b.WriteString(fmt.Sprintf("[%d][gid:%d] func: %s\t%s:%d", len(ts.infos), gid, runtime.FuncForPC(funcName).Name(), filepath.Base(file), line))
		_gLogger.TracePrintf("unlock : %s\n", b.String())
	}
}
